﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using MadLibrary.IO;

namespace MadLibrary.Text.EPub
{
    /// <summary>
    /// Used for IO operations on ePub format eBooks.
    /// </summary>
    public class EPubFile
    {
        /// <summary>
        /// Constructs a new ePub book.
        /// PublicationDate is set to today because it's a new eBook instance.
        /// The secondery constructor (that is used for loading ePubs) calls this one and overrides the date if needed.
        /// </summary>
        private EPubFile()
        {
            this.Authors = new List<string>();
            this.Subjects = new List<string>();
            this.Titles = new List<string>();
            this.Rights = new List<string>();
            this.Languages = new List<string>();
            this.Descriptions = new List<string>();
            this.PublicationDates = new List<DateTime>();
            this.Publishers = new List<string>();
            this.Contributors = new List<string>();

            this.XHtmlPages = new Dictionary<string, string>();
            this.CssFiles = new Dictionary<string, string>();
            this.Images = new Dictionary<string, ImageContainer>();
        }

        /// <summary>
        /// Takes the given ePub (in zip) and content file (content) and loads them into a new ePub file.
        /// </summary>
        /// <param name="zip">The ePub file loaded into a <see cref="MadLibrary.IO.ZipStorer"/>.</param>
        /// <param name="content">The content file used in the ePub eBook.</param>
        /// <param name="opfPath">The in-zip path of the opf document.</param>
        private EPubFile(ZipStorer zip, MemoryStream content, string opfPath) : this()
        {
            XElement opfContent = XElement.Load(content);

            XElement metadata = opfContent.Elements().First(e => e.Name.LocalName == "metadata");
            XElement manifest = opfContent.Elements().First(e => e.Name.LocalName == "manifest");
            this.LoadMetadata(metadata);
            this.LoadManifest(manifest, zip, opfPath);
        }

        /// <summary>
        /// Gets or sets the ID of the eBook.
        /// </summary>
        public string UUID { get; set; }
        /// <summary>
        /// Gets or sets a list of authors.
        /// </summary>
        public List<string> Authors { get; set; }
        /// <summary>
        /// Gets or sets a list of categories.
        /// </summary>
        public List<string> Subjects { get; set; }
        /// <summary>
        /// Gets or sets the publication date of the eBook.
        /// </summary>
        /// <remarks>Might be null.</remarks>
        public List<DateTime> PublicationDates { get; set; }
        /// <summary>
        /// Gets or sets the title of the eBook.
        /// </summary>
        public List<string> Titles { get; set; }
        /// <summary>
        /// Gets or sets the name of the publisher.
        /// </summary>
        public List<string> Publishers { get; set; }
        /// <summary>
        /// Gets or sets the rights of this document.
        /// </summary>
        public List<string> Rights { get; set; }
        /// <summary>
        /// Gets or sets the language of the document.
        /// </summary>
        public List<string> Languages { get; set; }
        /// <summary>
        /// Gets or sets a list of descriptions.
        /// </summary>
        public List<string> Descriptions { get; set; }
        /// <summary>
        /// Gets or sets a list of contributors.
        /// </summary>
        public List<string> Contributors { get; set; }
        /// <summary>
        /// Gets or sets the NcxDocument of this eBook.
        /// </summary>
        public NcxDocument NcxDocument { get; set; }
        /// <summary>
        /// Gets or sets a dictionary of XHtml strings.
        /// The key is the string name.
        /// </summary>
        public Dictionary<string, string> XHtmlPages { get; set; }
        /// <summary>
        /// Gets or sets a dictionary of css strings.
        /// The key is the string name.
        /// </summary>
        public Dictionary<string, string> CssFiles { get; set; }
        /// <summary>
        /// Gets or sets a dictionary of images.
        /// The key is the image name.
        /// </summary>
        public Dictionary<string, ImageContainer> Images { get; set; }
        
        /// <summary>
        /// Loads an ePub file from the given stream.
        /// </summary>
        /// <param name="file">The stream containing the ePub file.</param>
        /// <returns>A new EPubFile instance containing the data of the ePub.</returns>
        public static EPubFile Load(Stream file)
        {
            EPubFile ePub = null;

            // Initialy, each ePub file is a renamed ( re-extensioned, really ) ZIP file,
            // so firstly we need to open it as a zip file.
            using (var zip = ZipStorer.Open(file, FileAccess.Read))
            {
                // The standart of ePub 3 demends the file to have a META-INF folder
                // containing a container.xml file with the path to the start of the book.
                var files = zip.ReadCentralDir();
                
                if (files.Count(a => a.FilenameInZip.TrimEnd('\\', '/') == XmlStrings.ContainerPath) == 0)
                    throw new InvalidDataException(ErrorMessages.NoMetaOrContent);

                // Here we extract the content using nscoder's ZipStorer class
                // and put it into a XmlTextReader through a memort stream.
                var content = new MemoryStream();
                if (!zip.ExtractFile(files.Where(a => a.FilenameInZip.TrimEnd('\\', '/') == XmlStrings.ContainerPath).First(), content))
                    throw new InvalidDataException(ErrorMessages.ZipLoadError); //All he wants is money and power.
                //This comment is not weird, and is essential for the program execution.

                //Please leave this comment until I finish this code.
                // -Gilnaa P.S. Sorry if I've undone some of your changes, I'll clean this file later.
                XmlReader reader = XmlReader.Create(content);
                reader.Read();
                // This loop stops when it reaches to the  'rootfile' element or when it reaches the end of the file.
                while (reader.NodeType != XmlNodeType.Element ||
                       reader.Name.ToLower() != XmlStrings.RootFileElementName) 
                {
                    if (!reader.Read()) 
                        break;
                }

                if (reader.Name.ToLower() != XmlStrings.RootFileElementName)
                    throw new InvalidDataException(ErrorMessages.NoRootFile);
                
                //Get the path of the content.opf file.
                //This file contains all the paths of the html, css and images files.
                string fullPath = reader.GetAttribute(XmlStrings.FullPath);
                //Load it into a memory stream.
                content.Dispose();
                content = new MemoryStream();

                if (!zip.ExtractFile(files.Where(a => a.FilenameInZip.TrimEnd('\\', '/') == fullPath).First(), content))
                    throw new InvalidDataException(ErrorMessages.InvalidContentPath);

                ePub = new EPubFile(zip, content, fullPath);
                content.Dispose();
            }
            return ePub;
        }
        /// <summary>
        /// Loads an EPubFile from the given file path.
        /// </summary>
        /// <param name="filePath">The path to the ePub file.</param>
        /// <returns>A new instance of EPubFile instance containing the data inside the given file.</returns>
        public static EPubFile Load(string filePath)
        {
            return Load(File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read));
        }
        /// <summary>
        /// Saves the given EPubFile into the given file path.
        /// </summary>
        /// <param name="ePub"></param>
        /// <param name="filePath"></param>
        internal static void Save(EPubFile ePub, string filePath)
        {
            ZipStorer zip = ZipStorer.Create(filePath, ePub.Titles.First() ?? "ePub book");
            
        }
        /// <summary>
        /// Creates a new empty EPubFile with the given title and authors.
        /// </summary>
        /// <param name="title">The title of the eBook.</param>
        /// <param name="authors">An optional list of authors.</param>
        /// <returns>A new empty instance of EPubFile.</returns>
        internal static EPubFile Create(string title, params string[] authors)
        {
            var b = new EPubFile();
            if (authors != null)
                b.Authors.AddRange(authors);
            b.Titles.Add(title);
            return b;
        }

        /// <summary>
        /// Loads a NcxDocument from the given XElement.
        /// </summary>
        /// <param name="ncx">The source of the Ncx Document.</param>
        public void LoadNcxDocument(XElement ncx)
        {
            NcxDocument ncxDoc = new NcxDocument();
            var head = ncx.Elements().First(e => e.Name.LocalName == XmlStrings.Head);// ("head");
            foreach (var meta in head.Elements())
            {
                string name = meta.Attribute("name").Value;
                string content = meta.Attribute("content").Value;
                switch (name)
                {
                    case "dtb:uid": ncxDoc.UID = content; break;
                    case "epub-creator": ncxDoc.Creator = content; break;
                    case "dtb:depth": ncxDoc.Depth = Int32.Parse(content); break;
                    case "dtb:totalPageCount": ncxDoc.TotalPageCount = Int32.Parse(content); break;
                    case "dtb:maxPageNumber": ncxDoc.MaxPageNumber = Int32.Parse(content); break;
                    default: break;
                }
            }

            ncxDoc.DocumentTitle = ncx.Elements().First(e => e.Name.LocalName == XmlStrings.DocumentTitle).Value;
            ncxDoc.DocumentAuthor = ncx.Elements().First(e => e.Name.LocalName == XmlStrings.DocumentAuthor).Value;
            foreach (var navP in ncx.Elements().First(e => e.Name.LocalName == XmlStrings.NavigationMap).Elements())
                ncxDoc.NavigationMap.Add(NavPoint.Load(navP));
            NcxDocument = ncxDoc;
        }
        /// <summary>
        /// Rebuilds the the cached NcxDocument acording to the updated details.
        /// </summary>
        public void RefreshNcxDocument()
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Loads the manifest data from the given XML node.
        /// </summary>
        /// <param name="manifest"></param>
        /// <param name="zip"></param>
        /// <param name="opfPath"></param>
        private void LoadManifest(XElement manifest, ZipStorer zip, string opfPath)
        {
            int lastDividerIdx = opfPath.LastIndexOf(Path.DirectorySeparatorChar);
            if (lastDividerIdx == -1)
                lastDividerIdx = opfPath.LastIndexOf(Path.AltDirectorySeparatorChar);
            string opfFolder = opfPath.Substring(0, lastDividerIdx + 1);
            foreach (var element in manifest.Elements())
            {
                string id = element.Attribute("id").Value;
                string href = element.Attribute("href").Value;

                var zipMetaData = zip.ReadCentralDir().First(zipMeta => zipMeta.FilenameInZip == string.Format("{0}{1}", opfFolder, href));
                using (MemoryStream ms = new MemoryStream())
                {
                    zip.ExtractFile(zipMetaData, ms);
                    switch (element.Attribute("media-type").Value)
                    {
                        case MediaType.XHtml:
                            this.XHtmlPages.Add(id, ms.ReadAllText());
                            break;
                        case MediaType.Css:
                            this.CssFiles.Add(id, ms.ReadAllText());
                            break;
                        case MediaType.ImagePNG:
                        case MediaType.ImageJPEG:
                            this.Images.Add(id, new ImageContainer 
                            { 
                                ImageStream = ms.ToMemoryStream(), 
                                ImageType = element.Attribute(XmlStrings.MediaType).Value
                            });
                            break;
                        case MediaType.NCX:
                            LoadNcxDocument(XElement.Load(ms));
                            break;
                        default:
                            break;
                    }
                }
            }
        }
        /// <summary>
        /// Loads the metadata of the current ePub file from the given XML node.
        /// </summary>
        /// <param name="metadata"></param>
        private void LoadMetadata(XElement metadata)
        {
            foreach (var metaElement in metadata.Elements())
            {
                string metaContent = metaElement.Value;
                switch (metaElement.Name.LocalName)
                {
                    case "title":
                        {
                            this.Titles.Add(metaContent);
                            break;
                        }
                    case "rights":
                        {
                            this.Rights.Add(metaContent);
                            break;
                        }
                    case "description":
                        {
                            this.Descriptions.Add(metaContent);
                            break;
                        }
                    case "contributor":
                        {
                            this.Contributors.Add(metaContent);
                            break;
                        }
                    case "type":
                        {
                            break;
                        }
                    case "creator":
                        {
                            this.Authors.Add(metaContent);
                            break;
                        }
                    case "date":
                        {
                            this.PublicationDates.Add(DateTime.Parse(metaContent));
                            break;
                        }
                    case "publisher":
                        {
                            this.Publishers.Add(metaContent);
                            break;
                        }
                    case "subject":
                        {
                            this.Subjects.Add(metaContent);
                            break;
                        }
                    case "identifier":
                        {
                            this.UUID = metaContent;
                            break;
                        }
                    case "language":
                        {
                            break;
                        }
                }
            }
        }

        /// <summary>
        /// Contains the possible media type values of the manifest's nodes.
        /// </summary>
        static class MediaType
        {
            internal const string XHtml = @"application/xhtml+xml";
            internal const string Css = @"text/css";
            internal const string ImagePNG = @"image/png";
            internal const string ImageJPEG = @"image/jpeg";
            internal const string NCX = @"application/x-dtbncx+xml";
        }
        /// <summary>
        /// Contains the possible error messages.
        /// </summary>
        static class ErrorMessages
        {
            internal const string NoMetaOrContent = "The given ePub file is missing the standart 'META-INF' directory or 'META-INF/container.xml' file.";
            internal const string ZipLoadError = "Unable to read 'container.xml'. ePub might be corrupt.";
            internal const string InvalidContentPath = "The content.xml files points to an un-existant root file.";
            internal const string NoRootFile = "No 'rootfile' element has been found in the content file.";

        }
        /// <summary>
        /// Contains the used XML paths and elements or attributes names.
        /// </summary>
        static class XmlStrings
        {
            internal const string ContainerPath = @"META-INF/container.xml";
            internal const string RootFileElementName = @"rootfile";
            internal const string XMLNamespace = @"xmlns";
            internal const string FullPath = @"full-path";
            internal const string MediaType = @"media-type";
            internal const string Head = @"head";
            internal const string DocumentTitle = @"docTitle";
            internal const string DocumentAuthor = @"docAuthor";
            internal const string NavigationMap = @"navMap";
        }
    }
}